package com.linking.redis.manager;

import cn.hutool.core.util.StrUtil;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;

/**
 * @Author YaoWeiXin
 * @Date 2020/4/13 20:16
 * @Description redis工具虚类
 */
@Slf4j
public abstract class AbstractRedisUtils {

  @Resource
  AbstractRedisManager abstractRedisManager;

  @SuppressWarnings("unchecked")
  private RedisTemplate<String, Object> getTemplate() {
    String name = AbstractRedisManager.getCurName();
    if (StrUtil.isEmpty(name)) {
      name = abstractRedisManager.getBeanPrimary();
    }
    return (RedisTemplate<String, Object>) abstractRedisManager.getTemplate(name);
  }

  /**
   * 为给定 key 设置生存时间，当 key 过期时(生存时间为 0 )，它会被自动删除。
   *
   * @param key     键名
   * @param timeout 生存时间
   */
  public void expire(String key, long timeout) {
    getTemplate().expire(key, timeout, TimeUnit.SECONDS);
  }

  /**
   * 指定键名是否存在
   *
   * @param key 键名
   * @return 结果(true - 存在, false - 不存在)
   */
  public boolean exists(String key) {
    Boolean exists = getTemplate().hasKey(key);
    if (Objects.nonNull(exists)) {
      return exists;
    }
    return false;
  }

  /**
   * 获取指定键名的值
   *
   * @param key 键名
   * @return 值
   */
  public Object get(String key) {
    return getTemplate().opsForValue().get(key);
  }

  /**
   * 将字符串值 value 关联到 key
   *
   * @param key   键名
   * @param value 值
   */
  public void set(String key, Object value) {
    getTemplate().opsForValue().set(key, value);
  }

  /**
   * 将字符串值 value 关联到 key,并设置生命周期
   *
   * @param key        键名
   * @param value      值
   * @param expireTime 生命周期
   * @param timeUnit   时间单位
   */
  public void set(String key, Object value, Long expireTime, TimeUnit timeUnit) {
    getTemplate().opsForValue().set(key, value, expireTime, timeUnit);
  }

  /**
   * 将 key 的值设为 value ，当且仅当 key 不存在。
   *
   * @param key        键名
   * @param value      值
   * @param expireTime 生命周期
   * @param timeUnit   时间单位
   */
  public boolean setIfAbsent(String key, Object value, Long expireTime, TimeUnit timeUnit) {
    return Optional
        .ofNullable(getTemplate().opsForValue().setIfAbsent(key, value, expireTime, timeUnit))
        .orElse(false);
  }

  /**
   * 删除Key
   *
   * @param keys 键名,可以是多个
   */
  public void remove(String... keys) {
    for (String key : keys) {
      getTemplate().delete(key);
    }
  }

  /**
   * 删除指定前缀的Key
   *
   * @param pattern 前缀
   */
  public void removePattern(String pattern) {
    Set<String> keys = getTemplate().keys(pattern);
    if (keys != null && keys.size() > 0) {
      getTemplate().delete(keys);
    }
  }

  /**
   * 将 value 增加到 key
   *
   * @param key   键名
   * @param value 值
   */
  public Double incr(String key, double value) {
    return incr(key, value, 0L);
  }

  /**
   * 将 value 增加到 key
   *
   * @param key   键名
   * @param value 值
   */
  public Double incr(String key, double value, long time) {
    Double v = getTemplate().opsForValue().increment(key, value);
    if (time > 0) {
      expire(key, time);
    }
    return v;
  }

  /**
   * 为哈希表 key 中的域 field 的值加上增量 increment 。 增量也可以为负数，相当于对给定域进行减法操作。
   *
   * @param key     键名
   * @param hashKey 哈希键名
   * @param value   值
   */
  public Long hIncrement(String key, Object hashKey, Long value) {
    return getTemplate().opsForHash().increment(key, hashKey, value);
  }

  /**
   * 同时将多个 field-value (域-值)对设置到哈希表 key 中 此命令会覆盖哈希表中已存在的域。 如果 key 不存在，一个空哈希表被创建并执行 HMSET 操作。
   *
   * @param key     键名
   * @param hashKey 哈希键名
   * @param value   值
   */
  public void hmSet(String key, Object hashKey, Object value) {
    getTemplate().opsForHash().put(key, hashKey, value);
  }

  /**
   * 同时将多个 field-value (域-值)对设置到哈希表 key 中 此命令会覆盖哈希表中已存在的域。 如果 key 不存在，一个空哈希表被创建并执行 HMSET 操作。
   *
   * @param key  键名
   * @param maps hash
   */
  public void hmSetAll(String key, Map<String, Object> maps) {
    getTemplate().opsForHash().putAll(key, maps);
  }

  /**
   * 返回哈希表 key 中，一个或多个给定域的值。 如果给定的域不存在于哈希表，那么返回一个 nil 值。 因为不存在的 key 被当作一个空哈希表来处理，所以对一个不存在的 key 进行
   * HMGET 操作将返回一个只带有 nil 值的表。
   *
   * @param key     键名
   * @param hashKey 哈希键名
   * @return 值
   */
  public Object hmGet(String key, Object hashKey) {
    return getTemplate().opsForHash().get(key, hashKey);
  }

  /**
   * 返回哈希表 key 中，一个或多个给定域的值。 如果给定的域不存在于哈希表，那么返回一个 nil 值。 因为不存在的 key 被当作一个空哈希表来处理，所以对一个不存在的 key 进行
   * HMGET 操作将返回一个只带有 nil 值的表。
   *
   * @param key     键名
   * @param hashKey 哈希键名集合
   * @return 值
   */
  public List<Object> hmGetAny(String key, List<Object> hashKey) {
    return getTemplate().opsForHash().multiGet(key, hashKey);
  }

  /**
   * 返回哈希表 key 中，所有的值。
   * <p>
   * 在返回值里，紧跟每个域名(field name)之后是域的值(value)，所以返回值的长度是哈希表大小的两倍。
   *
   * @param key 键名
   * @return 值
   */
  public List<Object> hGetAll(String key) {
    return getTemplate().opsForHash().values(key);
  }

  /**
   * 返回哈希表 key 中域的数量。
   *
   * @param key 键名
   * @return 数量
   */
  public Long hSize(String key) {
    return getTemplate().opsForHash().size(key);
  }

  /**
   * 返回哈希表 key 中的所有域。
   *
   * @param key 哈希表
   * @return 所有域
   */
  public Set<Object> hmKeys(String key) {
    return getTemplate().opsForHash().keys(key);
  }

  /**
   * 删除哈希表 key 中的一个或多个指定域，不存在的域将被忽略。
   *
   * @param masterKey 键名
   * @param hashKey   哈希键名
   */
  public void hmDel(String masterKey, Object... hashKey) {
    getTemplate().opsForHash().delete(masterKey, hashKey);
  }

  /**
   * 删除哈希表 key 中的一个或多个指定域，不存在的域将被忽略。
   *
   * @param masterKey 键名
   * @param hashKey   哈希键名
   */
  public Boolean hmExists(String masterKey, Object hashKey) {
    return getTemplate().opsForHash().hasKey(masterKey, hashKey);
  }

  /**
   * 将哈希表 key 中的域 field 的值设置为 value ，当且仅当域 field 不存在。
   *
   * @param key       键名
   * @param hashKey   哈希键名
   * @param value     值
   */
  public Boolean hmPutIfAbsent(String key, Object hashKey, Object value) {
    return getTemplate().opsForHash().putIfAbsent(key, hashKey, value);
  }

  /**
   * 返回哈希表 key 中，所有的域和值。
   *
   * @param key       键名
   */
  public Map<String, Object> entries(String key) {
    HashOperations<String, String, Object> hash = getTemplate().opsForHash();
    return hash.entries(key);
  }

  /**
   * 将一个或多个值 value 插入到列表 key 的表头
   * <p>
   * 如果有多个 value 值，那么各个 value 值按从左到右的顺序依次插入到表头： 比如说，对空列表 mylist 执行命令 LPUSH mylist a b c ，列表的值将是 c b
   * a ， 这等同于原子性地执行 LPUSH mylist a 、 LPUSH mylist b 和 LPUSH mylist c 三个命令。
   * <p>
   * 如果 key 不存在，一个空列表会被创建并执行 LPUSH 操作。
   * <p>
   * 当 key 存在但不是列表类型时，返回一个错误。
   *
   * @param key   键名
   * @param value 值
   */
  public void lPush(String key, Object value) {
    getTemplate().opsForList().leftPush(key, value);
  }

  /**
   * 将一个或多个值 value 插入到列表 key 的表尾(最右边)。
   * <p>
   * 如果有多个 value 值，那么各个 value 值按从左到右的顺序依次插入到表尾： 比如对一个空列表 mylist 执行 RPUSH mylist a b c ，得出的结果列表为 a b
   * c ， 等同于执行命令 RPUSH mylist a 、 RPUSH mylist b 、 RPUSH mylist c 。
   * <p>
   * 如果 key 不存在，一个空列表会被创建并执行 RPUSH 操作。
   * <p>
   * 当 key 存在但不是列表类型时，返回一个错误。
   *
   * @param key   键名
   * @param value 值
   */
  public void rPush(String key, Object value) {
    getTemplate().opsForList().rightPush(key, value);
  }

  /**
   * 返回列表 key 中指定区间内的元素，区间以偏移量 start 和 stop 指定。
   * <p>
   * 下标(index)参数 start 和 stop 都以 0 为底，也就是说，以 0 表示列表的第一个元素，以 1 表示列表的第二个元素，以此类推。
   * <p>
   * 你也可以使用负数下标，以 -1 表示列表的最后一个元素， -2 表示列表的倒数第二个元素，以此类推。
   * <p>
   * 注意LRANGE命令和编程语言区间函数的区别
   * <p>
   * 假如你有一个包含一百个元素的列表，对该列表执行 LRANGE list 0 10 ，结果是一个包含11个元素的列表， 这表明 stop 下标也在 LRANGE
   * 命令的取值范围之内(闭区间)，这和某些语言的区间函数可能不一致， 比如Ruby的 Range.new 、 Array#slice 和Python的 range() 函数。
   * <p>
   * 超出范围的下标
   * <p>
   * 超出范围的下标值不会引起错误。
   * <p>
   * 如果 start 下标比列表的最大下标 end ( LLEN list 减去 1 )还要大，那么 LRANGE 返回一个空列表。
   * <p>
   * 如果 stop 下标比 end 下标还要大，Redis将 stop 的值设置为 end 。
   *
   * @param key   键名
   * @param start 开始下标
   * @param end   结束下标
   * @return 集合
   */
  public List<Object> lRange(String key, long start, long end) {
    return getTemplate().opsForList().range(key, start, end);
  }

  /**
   * 对一个列表进行修剪(trim)，就是说，让列表只保留指定区间内的元素，不在指定区间之内的元素都将被删除。
   * <p>
   * 举个例子，执行命令 LTRIM list 0 2 ，表示只保留列表 list 的前三个元素，其余元素全部删除。
   * <p>
   * 下标(index)参数 start 和 stop 都以 0 为底，也就是说，以 0 表示列表的第一个元素，以 1 表示列表的第二个元素，以此类推。
   * <p>
   * 你也可以使用负数下标，以 -1 表示列表的最后一个元素， -2 表示列表的倒数第二个元素，以此类推。
   * <p>
   * 当 key 不是列表类型时，返回一个错误。
   * <p>
   * LTRIM 命令通常和 LPUSH 命令或 RPUSH 命令配合使用，举个例子：
   * <p>
   * LPUSH log newest_log LTRIM log 0 99
   *
   * @param key   键名
   * @param start 开始下标
   * @param end   结束下标
   */
  public void lTrim(String key, long start, long end) {
    getTemplate().opsForList().trim(key, start, end);
  }

  /**
   * 将一个或多个 member 元素加入到集合 key 当中，已经存在于集合的 member 元素将被忽略。
   * <p>
   * 假如 key 不存在，则创建一个只包含 member 元素作成员的集合。
   * <p>
   * 当 key 不是集合类型时，返回一个错误。
   *
   * @param key   键名
   * @param value 值
   */
  public void add(String key, Object value) {
    getTemplate().opsForSet().add(key, value);
  }

  /**
   * 返回集合 key 中的所有成员。
   * <p>
   * 不存在的 key 被视为空集合。
   *
   * @param key 键名
   * @return 集合
   */
  public Set<Object> members(String key) {
    return getTemplate().opsForSet().members(key);
  }

  /**
   * 返回有序集 key 的基数。
   *
   * @param key 键名
   */
  public Long zCard(String key) {
    return getTemplate().opsForZSet().zCard(key);
  }

  /**
   * 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。
   * <p>
   * 如果某个 member 已经是有序集的成员，那么更新这个 member 的 score 值，并通过重新插入这个 member 元素，来保证该 member 在正确的位置上。
   * <p>
   * score 值可以是整数值或双精度浮点数。
   * <p>
   * 如果 key 不存在，则创建一个空的有序集并执行 ZADD 操作。
   * <p>
   * 当 key 存在但不是有序集类型时，返回一个错误。
   *
   * @param key   键名
   * @param value 值
   * @param score 得分
   */
  public void zAdd(String key, Object value, double score) {
    getTemplate().opsForZSet().add(key, value, score);
  }

  /**
   * 移除有序集 key 中的一个或多个成员，不存在的成员将被忽略。
   * <p>
   * 当 key 存在但不是有序集类型时，返回一个错误。
   *
   * @param key    键名
   * @param values 值
   */
  public void zDel(String key, Object... values) {
    getTemplate().opsForZSet().remove(key, values);
  }

  /**
   * 为有序集 key 的成员 member 的 score 值加上增量 increment 。
   * <p>
   * 可以通过传递一个负数值 increment ，让 score 减去相应的值，比如 ZINCRBY key -5 member ，就是让 member 的 score 值减去 5 。
   * <p>
   * 当 key 不存在，或 member 不是 key 的成员时， ZINCRBY key increment member 等同于 ZADD key increment member 。
   * <p>
   * 当 key 不是有序集类型时，返回一个错误。
   * <p>
   * score 值可以是整数值或双精度浮点数。
   *
   * @param key   键名
   * @param value 值
   * @param score 得分
   */
  public void zIncrement(String key, Object value, double score) {
    getTemplate().opsForZSet().incrementScore(key, value, score);
  }

  /**
   * 返回有序集 key 中， score 值介于 max 和 min 之间(默认包括等于 max 或 min )的所有的成员。 有序集成员按 score 值递减(从大到小)的次序排列。
   * <p>
   * 具有相同 score 值的成员按字典序的逆序(reverse lexicographical order )排列。
   * <p>
   * 除了成员按 score 值递减的次序排列这一点外， ZREVRANGEBYSCORE 命令的其他方面和 ZRANGEBYSCORE 命令一样。
   *
   * @param key 键名
   * @param min 最小值
   * @param max 最大值
   * @return 集合
   */
  public Set<Object> rangeByScore(String key, double min, double max) {
    return getTemplate().opsForZSet().rangeByScore(key, min, max);
  }

  /**
   * 返回有序集 key 中，指定区间内的成员。
   * <p>
   * 其中成员的位置按 score 值递减(从大到小)来排列。 具有相同 score 值的成员按字典序的逆序(reverse lexicographical order)排列。 除了成员按
   * score 值递减的次序排列这一点外， ZREVRANGE 命令的其他方面和 ZRANGE 命令一样。
   *
   * @param key   键名
   * @param start 开始下标
   * @param end   结束下标
   * @return 集合
   */
  public Set<Object> rangeByRev(String key, long start, long end) {
    return getTemplate().opsForZSet().reverseRange(key, start, end);
  }

  /**
   * 移除并返回列表 key 的头元素。
   *
   * @param key 键名
   * @return 头元素
   */
  public Object lPop(String key) {
    return getTemplate().opsForList().leftPop(key);
  }

  /**
   * 以秒为单位，返回给定 key 的剩余生存时间(TTL, time to live)。 返回值： 当 key 不存在时，返回 -2 。 当 key 存在但没有设置剩余生存时间时，返回 -1 。
   * 否则，以秒为单位，返回 key 的剩余生存时间。
   *
   * @param key 键名
   * @return 剩余生存时间
   */
  public long ttl(String key) {
    Long ttl = getTemplate().getExpire(key);
    if (Objects.nonNull(ttl)) {
      return ttl;
    }
    return -2;
  }

  /**
   * 执行LUA脚本
   *
   * @param script  脚本
   * @param keyList KEY集合
   * @param arg     参数集合
   */
  public void execute(RedisScript<Boolean> script, List<String> keyList, Object... arg) {
    getTemplate().execute(script, keyList, arg);
  }
}